<?php

namespace CommonBundle\Parser;

use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Query\Parameter;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\ExpressionLanguage\Node\BinaryNode;
use Symfony\Component\ExpressionLanguage\Node\ConstantNode;
use Symfony\Component\ExpressionLanguage\Node\GetAttrNode;
use Symfony\Component\ExpressionLanguage\Node\Node;
use Symfony\Component\ExpressionLanguage\Node\UnaryNode;
use Symfony\Component\Validator\Exception\ValidatorException;

class ExpressionDqlParser
{
    private $expression = '';
    private $values = [];
    private $names = [];
    private $source = '';
    private $dataClass = '';

    private $joins = [];
    /** @var ArrayCollection $parameters */
    private $parameters = null;

    // Constant
    private static $expressionSignature = 'entity';
    private static $signature = 'filter_entity';
    private static $parameterPrefix = 'filter_parameter_';
    private static $logicOperators = [
        '&&', '||'
    ];
    private static $operators = [
        '==' => '%1$s = %2$s',
        '!=' => '%1$s != %2$s',
        '>'  => '%1$s > %2$s',
        '>='  => '%1$s >= %2$s',
        '<'  => '%1$s < %2$s',
        '<='  => '%1$s <= %2$s',
        '!'  => '%1$s NOT %2$s',
        '&&' => '%1$s AND %2$s',
        '||' => '%1$s OR %2$s',
        '+' => '%1$s + %2$s',
        '-' => '%1$s - %2$s',
        '*' => '%1$s * %2$s',
        '/' => '%1$s / %2$s',
        'matches' => 'REGEXP(%1$s, %2$s) = TRUE',
    ];

    public function __construct()
    {
        $this->reset();
    }

    /**
     * @param string $expression
     * @return $this
     */
    public function setExpression(string $expression): ExpressionDqlParser
    {
        $this->expression = $expression;
        return $this;
    }

    /**
     * @param string $dataClass
     * @return $this
     */
    public function setDataClass(string $dataClass): ExpressionDqlParser
    {
        $this->dataClass = $dataClass;
        return $this;
    }

    /**
     * @param array $values
     * @return $this
     */
    public function setValues(array $values = []): ExpressionDqlParser
    {
        $this->values = array_merge(
            [self::$expressionSignature => ''], // dummy here
            $values
        );
        $this->names = array_keys($this->values);
        return $this;
    }

    /**
     * Adds a raw string to the compiled code.
     *
     * @param string $string The string
     *
     * @return $this
     */
    public function raw(string $string): ExpressionDqlParser
    {
        $this->source .= $string;

        return $this;
    }

    /**
     * Adds a quoted string to the compiled code.
     *
     * @param string $value The string
     *
     * @return $this
     */
    public function string(string $value): ExpressionDqlParser
    {
        $this->source .= sprintf('"%s"', addcslashes($value, "\0\t\"\$\\"));

        return $this;
    }

    /**
     * @return $this
     */
    public function reset(): ExpressionDqlParser
    {
        $this->source = '';
        $this->dataClass = '';
        $this->expression = '';
        $this->values = [];
        $this->names = [];
        $this->parameters = new ArrayCollection();
        return $this;
    }

    /**
     * @return $this
     */
    public function compile(): ExpressionDqlParser
    {
        $expressionLanguage = new ExpressionLanguage();
        $parsed = $expressionLanguage->parse($this->expression, $this->names);
        $dql = $this->recursiveAST($parsed->getNodes());
        $this
            ->raw($this->getHeader())
            ->raw($this->processJoins())
            ->raw($dql);

        return $this;
    }

    /**
     * @return string
     */
    public function getSource(): string
    {
        return $this->source;
    }

    /**
     * @return string
     */
    public function getDataClass(): string
    {
        return $this->dataClass;
    }

    /**
     * @return string
     */
    public function getHeader(): string
    {
        return sprintf("SELECT %s FROM %s AS %s ",
            self::$signature, $this->dataClass, self::$signature);
    }

    /**
     * @return array
     */
    public function getJoins(): array
    {
        return $this->joins;
    }

    /**
     * @return string
     */
    public function processJoins(): string
    {
        $dqls = '';
        foreach ($this->joins as $key => $value) {
            $dqls .= sprintf('LEFT JOIN %s AS %s ', $value, $key);
        }
        return $dqls;
    }

    /**
     * @return ArrayCollection
     */
    public function getParameters(): ArrayCollection
    {
        return $this->parameters;
    }

    /**
     * @param Node $node
     * @param int $deep
     * @return string
     */
    private function recursiveAST(Node $node, int $deep = 0): string
    {
        //dump($node);

        $raw = function(string &$string, string $input) {
            $string .= $input;
            return $string;
        };

        $nodes = $node->nodes;
        $output = '';

        // Check is grouped
        $isGrouped =
            count($nodes)
            && !($node instanceof GetAttrNode);

        // Begin
        if($deep == 0) $raw($output, 'WHERE ');
        if($isGrouped) $raw($output, '(');

        // Recursive

        if($node instanceof BinaryNode) {
            $operator = $node->attributes['operator'];

            $left = $this->recursiveAST($node->nodes['left'], $deep + 1);
            $right = $this->recursiveAST($node->nodes['right'], $deep + 1);

            // Logic operators single attr getter checker
            if(in_array($operator, self::$logicOperators)) {
                if($node->nodes['left'] instanceof GetAttrNode) {
                    $raw($left, ' IS NOT NULL');
                }
                if($node->nodes['right'] instanceof GetAttrNode) {
                    $raw($right, ' IS NOT NULL');
                }
            }

            $raw($output, sprintf(self::$operators[$operator], $left, $right));
        }

        elseif($node instanceof ConstantNode) {
            $index = $this->parameters->count() + 1;
            $this->parameters->add(
                new Parameter(
                    self::$parameterPrefix.$index,
                    $node->attributes['value']
                )
            );

            $raw($output, ":".self::$parameterPrefix.$index);
        }

        elseif($node instanceof UnaryNode) {
            if($node->attributes['operator'] == '!') {
                foreach ($nodes as $nextNode) {
                    $raw($output, ' '. $this->recursiveAST($nextNode, $deep + 1) .' ');
                }
                $raw($output, ' IS NULL ');
            }
        }

        elseif($node instanceof GetAttrNode) {
            $dump = $node->dump();
            $exportValue = '';
            $pattern = sprintf('/^%s(\.get([A-Z]\w+)\(\))+$/', self::$expressionSignature);
            if(preg_match($pattern, $dump, $match)) {
                $pattern = '/\.get([A-Z]\w+)/';
                if(preg_match_all($pattern, $dump, $matches)) {
                    $joinKey = self::$signature;
                    // $lastJoinClass = $this->getDataClass();

                    foreach ($matches[1] as $i => $match) {
                        $property = lcfirst($match);

                        /*
                        if($lastJoinClass) {
                            $reflect = new \ReflectionClass($lastJoinClass);
                            if($reflect->hasProperty($property)) {
                                $docReader = new AnnotationReader();
                                $annotations = $docReader->getPropertyAnnotations($reflect->getProperty($property));
                                // dump($annotations);
                            }
                        }
                        */

                        $exportValue = $joinKey . '.' . $property;
                        $joinKey .= '_' . $property;

                        if($i >= count($matches[1]) -1) break;
                        $this->joins[$joinKey] = $exportValue;
                    }
                }
                else {
                    throw new ValidatorException('Mismatch.');
                }
            }
            else {
                // Set parameters
                $index = $this->parameters->count() + 1;
                $this->parameters->add(
                    new Parameter(
                        self::$parameterPrefix.$index,
                        $node->evaluate(null, $this->values)
                    )
                );
                $raw($output, ":".self::$parameterPrefix.$index);
            }

            $raw($output, $exportValue);
        }
        else {
            foreach ($nodes as $nextNode) {
                $raw($output, ' '. $this->recursiveAST($nextNode, $deep + 1) .' ');
            }
        }

        // End
        if($isGrouped) $raw($output, ')');

        return $output;
    }
}